﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using LaunchMeNot.DAL;
using Lime49.Exceptions;
using Microsoft.Win32;
using IWshRuntimeLibrary;

namespace LaunchMeNot.Core
{
    public class LaunchMeNotBLL
    {
        /// <summary>
        /// Gets the path of the application executable.
        /// </summary>
        /// <returns>The path of the application executable.</returns>
        private static string GetAppPath()
        {
            string appPath = Assembly.GetEntryAssembly().Location;
            return appPath;
        }

        /// <summary>
        /// Gets the path to the file, directory, or registry key used to store a type of entry.
        /// </summary>
        /// <param name="entryType">Type of the entry.</param>
        /// <returns>The file, directory or registry path where items of this type are stored, or an empty string for unknown values.</returns>
        public static string GetPath(StartupEntryType entryType)
        {
            switch (entryType) {
                case StartupEntryType.AllUsersStartup:
                    return GetAllUsersStartMenuPath();
                case StartupEntryType.CurrentUserRun:
                    return @"Software\Microsoft\Windows\CurrentVersion\Run";
                case StartupEntryType.CurrentUserStartup:
                    return Environment.GetFolderPath(Environment.SpecialFolder.Startup);
                case StartupEntryType.LaunchMeNot:
                    return DALConfigurationManager.FilePath;
                case StartupEntryType.LocalMachineRun:
                    return @"Software\Microsoft\Windows\CurrentVersion\Run";
                default:
                    return string.Empty;
            }
        }

        [DllImport("shell32.dll")]
        static extern bool SHGetSpecialFolderPath(IntPtr hwndOwner,
        [Out] StringBuilder lpszPath, int nFolder, bool fCreate);
        /// <summary>
        /// Gets the path to the 'All Users\Startup' directory.
        /// </summary>
        /// <returns>The path to the common start menu startup directory.</returns>
        private static string GetAllUsersStartMenuPath()
        {
            StringBuilder path = new StringBuilder(260);
            const int CSIDL_COMMON_STARTUP = 0x0018; // All Users\Startup
            SHGetSpecialFolderPath(IntPtr.Zero, path, CSIDL_COMMON_STARTUP, false);
            return path.ToString();
        }

        /// <summary>
        /// Gets an entry store for a type of entry.
        /// </summary>
        /// <param name="entryType">Type of the entries to manage.</param>
        /// <returns>An appropriate entry store, or <c>null</c>.</returns>
        public static IEntryStore GetStore(StartupEntryType entryType) {
            IEntryStore store = null;
            switch(entryType) {
                case StartupEntryType.AllUsersStartup:
                case StartupEntryType.CurrentUserStartup:
                    store = new DirectoryStore(entryType);
                    break;
                case StartupEntryType.CurrentUserRun:
                case StartupEntryType.LocalMachineRun:
                    store = new RegistryStore(entryType);
                    break;
                case StartupEntryType.LaunchMeNot:
                    store = DALConfigurationManager.LaunchMeNotStore;
                    break;
                case StartupEntryType.None:
                    break;
            }
            return store;
        }

        #region AutoLaunch
        /// <summary>
        /// Starts launching entries.
        /// </summary>
        /// <param name="entriesToLaunch">The entries to launch.</param>
        /// <param name="refreshFrequency">The time in milliseconds to wait before updating the remaing launch delay for entries.</param>
        /// <param name="worker">A BackgroundWorker used to launch entries, or <c>null</c>.</param>
        public static void AutoLaunch(IEnumerable<PendingLaunchMeNotEntry> entriesToLaunch, int refreshFrequency, BackgroundWorker worker)
        {
            StringBuilder log = new StringBuilder(string.Format("\n----Starting AutoLaunch {0} {1}@{2}----\n", DateTime.Now.ToString("R"), Environment.UserName, Environment.MachineName));
            log.Append(string.Format("{0} entries to launch, {1}\n", entriesToLaunch.Count(), CoreConfigurationManager.AllowCancelAutoLaunch ? "abort allowed" : "abort not allowed"));

            double percentPerEntry = 100.0 / entriesToLaunch.Count();
            double percentComplete = 0;
            double lastLaunchDelay = 0;
            List<string> errors = new List<string>();
            foreach (PendingLaunchMeNotEntry entry in entriesToLaunch) {
                if (worker != null && worker.CancellationPending) {
                    log.Append(string.Format("----Launch Aborted at {0}----\n\n", DateTime.Now.ToString("R")));
                    goto LaunchDone;
                }
                log.Append(string.Format("Waiting {0} seconds before starting {1} ({2})\n", lastLaunchDelay, entry.Name, entry.FullPath));
                entry.LaunchDelayRemaining = lastLaunchDelay;
                lastLaunchDelay = entry.LaunchDelay;
                if (entry.LaunchDelay == 0) {
                    try {
                        Process.Start(new ProcessStartInfo(entry.Path, entry.Arguments));
                        entry.IsLaunched = true;
                    } catch (Exception ex) {
                        entry.ErrorMessage = ex.Message;
                        log.Append(string.Format("Error starting {0}: {1}\n", entry.Name, ex.Message));
                    }
                } else {
                    while (entry.LaunchDelayRemaining > 0) {
                        Thread.Sleep(refreshFrequency);
                        entry.LaunchDelayRemaining = Math.Max(entry.LaunchDelayRemaining - refreshFrequency / 1000.0, 0);
                        if (worker != null && worker.CancellationPending) {
                            log.Append(string.Format("----Launch Aborted at {0}----\n\n", DateTime.Now.ToString("R")));
                            goto LaunchDone;
                        }
                    }
                    try {
                        Process.Start(new ProcessStartInfo(entry.Path, entry.Arguments));
                        entry.IsLaunched = true;
                    } catch (Exception ex) {
                        entry.ErrorMessage = ex.Message;
                        log.Append(string.Format("Error starting {0}: {1}\n", entry.Name, ex.Message));
                    }
                }
                percentComplete += percentPerEntry;
                if (worker != null) {
                    worker.ReportProgress(Convert.ToInt32(percentComplete), entry);
                }
            }
            LaunchDone:
            if (worker == null || !worker.CancellationPending) {
                log.Append(string.Format("----Launch Completed at {0}----\n\n", DateTime.Now.ToString("R")));
            }

            if (CoreConfigurationManager.AutoLaunchLog) {
                string applicationDirectory = Lime49.Utils.GetApplicationPath();
                string logPath = null;
                if(!string.IsNullOrEmpty(CoreConfigurationManager.AutoLaunchLogPath)) {
                    if(CoreConfigurationManager.AutoLaunchLogPath.ToLowerInvariant().Contains("%appdir%")) {
                        logPath = CoreConfigurationManager.AutoLaunchLogPath.ToLowerInvariant().Replace("%appdir%", applicationDirectory);
                    } else {
                        logPath = CoreConfigurationManager.AutoLaunchLogPath;
                    }
                }
                if(string.IsNullOrEmpty(logPath)) {
                    // not configured or empty, use default log path
                    logPath = Path.Combine(applicationDirectory, "autolaunch.log");
                }
                using (StreamWriter writer = new StreamWriter(logPath, true, Encoding.UTF8)) {
                    writer.Write(log.ToString());
                }
            }
        }

        /// <summary>
        /// Gets whether LaunchMeNot is installed, and where abouts in the registry.
        /// </summary>
        /// <returns>The location where LaunchMeNot is installed, if it is installed.</returns>
        public static StartupEntryType IsLaunchMeNotInstalled()
        {
            StartupEntryType type = 0;
            IEntryStore store;
            IEnumerable<IStartupEntry> allEntries,
                                       existingInstallations;

            string appPath = GetAppPath();
            //todo: move this to a function
            try {
                store = GetStore(StartupEntryType.LocalMachineRun);
                allEntries = store.GetStartupEntries();
                existingInstallations = allEntries.Where(n => n.Path.Equals(appPath, StringComparison.InvariantCultureIgnoreCase) || n.Name.Contains("LaunchMeNot"));
                if (existingInstallations.Any()) {
                    type |= StartupEntryType.LocalMachineRun;
                }
            } catch { /*NoPermission*/ }

            try {
                store = GetStore(StartupEntryType.CurrentUserRun);
                allEntries = store.GetStartupEntries();
                existingInstallations = allEntries.Where(n => n.Path.Equals(appPath, StringComparison.InvariantCultureIgnoreCase) || n.Name.Contains("LaunchMeNot"));
                if (existingInstallations.Any()) {
                    type |= StartupEntryType.CurrentUserRun;
                }
            } catch { /*NoPermission*/ }

            try {
                store = GetStore(StartupEntryType.CurrentUserStartup);
                allEntries = store.GetStartupEntries();
                existingInstallations = allEntries.Where(n => n.Path.Equals(appPath, StringComparison.InvariantCultureIgnoreCase) || n.Name.Contains("LaunchMeNot"));
                if (existingInstallations.Any()) {
                    type |= StartupEntryType.CurrentUserStartup;
                }
            } catch { }

            try {
                store = GetStore(StartupEntryType.AllUsersStartup);
                allEntries = store.GetStartupEntries();
                existingInstallations = allEntries.Where(n => n.Path.Equals(appPath, StringComparison.InvariantCultureIgnoreCase) || n.Name.Contains("LaunchMeNot"));
                if (existingInstallations.Any()) {
                    type |= StartupEntryType.AllUsersStartup;
                }
            } catch { }

            return type;
        }

        /// <summary>
        /// Installs or uninstalls AutoLaunch on startup.
        /// </summary>
        /// <param name="requiredType">The required startup type.</param>
        public static void ToggleAutoLaunch(StartupEntryType requiredType) {
            StartupEntryType currentStatus = IsLaunchMeNotInstalled();
            MultipleException errors = new MultipleException();
            if(currentStatus != StartupEntryType.None)
                DisableAutoLaunch();
            IStartupEntry newEntry;
            IEntryStore store;

            string appPath = GetAppPath();

            try {
                if((requiredType & StartupEntryType.CurrentUserStartup) == StartupEntryType.CurrentUserStartup) {
                    store = new DirectoryStore(StartupEntryType.CurrentUserStartup);
                    newEntry = new ShellShortcut("LaunchMeNot AutoLaunch", appPath, "-l");
                    store.AddStartupEntry(newEntry);
                }
            } catch(Exception ex) {
                errors.Exceptions.Add(new Exception("Failed to add to current user startup", ex));
            }

            try {
                if((requiredType & StartupEntryType.AllUsersStartup) == StartupEntryType.AllUsersStartup) {
                    store = new DirectoryStore(StartupEntryType.AllUsersStartup);
                    newEntry = new ShellShortcut("LaunchMeNot AutoLaunch", appPath, "-l");
                    store.AddStartupEntry(newEntry);
                }
            } catch(Exception ex) {
                errors.Exceptions.Add(new Exception("Failed to add to all users startup", ex));
            }

            try {
                if((requiredType & StartupEntryType.CurrentUserRun) == StartupEntryType.CurrentUserRun) {
                    store = new RegistryStore(StartupEntryType.CurrentUserRun);
                    newEntry = new RegistryShortcut("LaunchMeNot AutoLaunch", appPath, "-l");
                    store.AddStartupEntry(newEntry);
                }
            } catch(Exception ex) {
                errors.Exceptions.Add(new Exception("Failed to add to current user registry startup", ex));
            }

            try {
                if((requiredType & StartupEntryType.LocalMachineRun) == StartupEntryType.LocalMachineRun) {
                    store = new RegistryStore(StartupEntryType.LocalMachineRun);
                    newEntry = new RegistryShortcut("LaunchMeNot AutoLaunch", appPath, "-l");
                    store.AddStartupEntry(newEntry);
                }
            } catch(Exception ex) {
                errors.Exceptions.Add(new Exception("Failed to add to local machine registry startup", ex));
            }

            if (errors.Exceptions.Any())
                throw errors;
        }

        /// <summary>
        /// Disables AutoLaunch on startup.
        /// </summary>
        private static void DisableAutoLaunch()
        {
            IEntryStore store;
            IEnumerable<IStartupEntry> existingInstallations;
            string appPath = GetAppPath();
            try {
                store = new RegistryStore(RegistryHive.LocalMachine, GetPath(StartupEntryType.LocalMachineRun));
                var allEntries = store.GetStartupEntries();
                existingInstallations = allEntries.Where(n => n.Path.Equals(appPath, StringComparison.InvariantCultureIgnoreCase) || n.Name.Contains("LaunchMeNot"));
                if (existingInstallations.Any()) {
                    store.DeleteStartupEntries(existingInstallations);
                }
            } catch { /*NoPermission*/ }

            try {
                store = new RegistryStore(RegistryHive.CurrentUser, GetPath(StartupEntryType.CurrentUserRun));
                var allEntries = store.GetStartupEntries();
                existingInstallations = allEntries.Where(n => n.Path.Equals(appPath, StringComparison.InvariantCultureIgnoreCase) || n.Name.Contains("LaunchMeNot"));
                if (existingInstallations.Any()) {
                    store.DeleteStartupEntries(existingInstallations);
                }
            } catch { /*NoPermission*/ }

            try {
                store = new DirectoryStore(StartupEntryType.CurrentUserStartup);
                var allEntries = store.GetStartupEntries();
                existingInstallations = allEntries.Where(n => n.Path.Equals(appPath, StringComparison.InvariantCultureIgnoreCase) || n.Name.Contains("LaunchMeNot"));
                if (existingInstallations.Any()) {
                    store.DeleteStartupEntries(existingInstallations);
                }
            } catch { }

            try {
                store = new DirectoryStore(StartupEntryType.AllUsersStartup);
                var allEntries = store.GetStartupEntries();
                existingInstallations = allEntries.Where(n => n.Path.Equals(appPath, StringComparison.InvariantCultureIgnoreCase) || n.Name.Contains("LaunchMeNot"));
                if (existingInstallations.Any()) {
                    store.DeleteStartupEntries(existingInstallations);
                }
            } catch { }
        }
        #endregion

        #region CRUD

        /// <summary>
        /// Adds startup entries.
        /// </summary>
        /// <param name="entries">The entries, which can be a mixture of supported types.</param>
        public static void AddStartupEntries(IEnumerable<IStartupEntry> entries) {
            StartupEntryCollection collection = FilterEntries(entries);
            IEntryStore store;
            if (collection.AllUsersStartmenu.Any()) {
                store = LaunchMeNotBLL.GetStore(StartupEntryType.AllUsersStartup);
                store.AddStartupEntries(collection.AllUsersStartmenu);
            }
            if (collection.CurrentUserStartmenu.Any()) {
                store = LaunchMeNotBLL.GetStore(StartupEntryType.CurrentUserStartup);
                store.AddStartupEntries(collection.CurrentUserStartmenu);
            }
            if (collection.CurrentUserRun.Any()) {
                store = LaunchMeNotBLL.GetStore(StartupEntryType.CurrentUserRun);
                store.AddStartupEntries(collection.CurrentUserRun);
            }
            if (collection.LocalMachineRunEntries.Any()) {
                store = LaunchMeNotBLL.GetStore(StartupEntryType.LocalMachineRun);
                store.AddStartupEntries(collection.LocalMachineRunEntries);
            }
            if (collection.LaunchMeNot.Any()) {
                store = LaunchMeNotBLL.GetStore(StartupEntryType.LaunchMeNot);
                store.AddStartupEntries(collection.LaunchMeNot);
            }
        }

        /// <summary>
        /// Edits a startup entry.
        /// </summary>
        /// <param name="entry">The entry.</param>
        public static void EditStartupEntry(IStartupEntry entry)
        {
            IEntryStore store = GetStore(entry.EntryType);
            if (store != null)
                store.EditStartupEntry(entry);
        }

        /// <summary>
        /// Deletes startup entries.
        /// </summary>
        /// <param name="entries">The entries, which can be a mixture of supported types.</param>
        public static void DeleteStartupEntries(IEnumerable<IStartupEntry> entries) {
            StartupEntryCollection collection = FilterEntries(entries);
            IEntryStore store;
            if (collection.AllUsersStartmenu.Any()) {
                store = LaunchMeNotBLL.GetStore(StartupEntryType.AllUsersStartup);
                store.DeleteStartupEntries(collection.AllUsersStartmenu);
            }
            if (collection.CurrentUserStartmenu.Any()) {
                store = LaunchMeNotBLL.GetStore(StartupEntryType.CurrentUserStartup);
                store.DeleteStartupEntries(collection.CurrentUserStartmenu);
            }
            if (collection.CurrentUserRun.Any()) {
                store = LaunchMeNotBLL.GetStore(StartupEntryType.CurrentUserRun);
                store.DeleteStartupEntries(collection.CurrentUserRun);
            }
            if (collection.LocalMachineRunEntries.Any()) {
                store = LaunchMeNotBLL.GetStore(StartupEntryType.LocalMachineRun);
                store.DeleteStartupEntries(collection.LocalMachineRunEntries);
            }
            if (collection.LaunchMeNot.Any()) {
                store = LaunchMeNotBLL.GetStore(StartupEntryType.LaunchMeNot);
                store.DeleteStartupEntries(collection.LaunchMeNot);
            }
        }

        /// <summary>
        /// Converts an entry to a different StartupEntryType.
        /// </summary>
        /// <param name="entry">The entry to convert.</param>
        /// <param name="targetType">The target entry type of the conversion.</param>
        public static void ConvertEntry(IStartupEntry entry, StartupEntryType targetType)
        {
            if (entry.EntryType == targetType)
                return;
            IEntryStore newStore = GetStore(targetType);
            IStartupEntry newEntry = null;
            switch (targetType) {
                case StartupEntryType.AllUsersStartup:
                case StartupEntryType.CurrentUserStartup:
                    newEntry = new ShellShortcut(entry);
                    break;
                case StartupEntryType.CurrentUserRun:
                case StartupEntryType.LocalMachineRun:
                    newEntry = new RegistryShortcut(entry);
                    break;
                case StartupEntryType.LaunchMeNot:
                    int maxLaunchOrder = ((XMLStore)newStore).GetMaxLaunchOrder();
                    newEntry = new LaunchMeNotEntry(entry) { LaunchOrder = maxLaunchOrder + 1 };
                    break;
                default:
                    throw new ArgumentException(string.Format("Unsupported conversion type: {0}", targetType.ToString()));
            }

            try {
                newStore.AddStartupEntry(newEntry);
            } catch (LaunchMeNotException lEx) {
                if (lEx.Code == 650) {
                    var existingEntries = newStore.GetStartupEntries();
                    IEnumerable<string> existingNames = existingEntries.Select(e => e.Name);
                    newEntry.Name = Utils.GetNextAvailableName(newEntry.Name, existingNames);
                    newStore.AddStartupEntry(newEntry);
                } else {
                    throw;
                }
            }

            // delete old entry after adding new one
            IEntryStore oldStore = GetStore(entry.EntryType);
            if (oldStore != null)
                oldStore.DeleteStartupEntry(entry);
        }

        /// <summary>
        /// Gets entries of a specified type or types.
        /// </summary>
        /// <param name="types">A bitwise combination of required entry types.</param>
        /// <returns>All entries of the specified types</returns>
        public static IEnumerable<IStartupEntry> GetEntries(StartupEntryType types)
        {

            List<IStartupEntry> entries = new List<IStartupEntry>();
            IEntryStore store;
            if ((types & StartupEntryType.AllUsersStartup) == StartupEntryType.AllUsersStartup) {
                store = LaunchMeNotBLL.GetStore(StartupEntryType.AllUsersStartup);
                var allUsersEntries = store.GetStartupEntries();
                foreach (var entry in allUsersEntries) {
                    entry.EntryType = StartupEntryType.AllUsersStartup;
                }
                entries.AddRange(allUsersEntries);
            }
            if ((types & StartupEntryType.CurrentUserStartup) == StartupEntryType.CurrentUserStartup) {
                store = LaunchMeNotBLL.GetStore(StartupEntryType.CurrentUserStartup);
                var currentUserStartupEntries = store.GetStartupEntries();
                foreach (var entry in currentUserStartupEntries) {
                    entry.EntryType = StartupEntryType.CurrentUserStartup;
                }
                entries.AddRange(currentUserStartupEntries);
            }
            if ((types & StartupEntryType.CurrentUserRun) == StartupEntryType.CurrentUserRun) {
                store = LaunchMeNotBLL.GetStore(StartupEntryType.CurrentUserRun);
                var lmRunEntries = store.GetStartupEntries();
                foreach (var entry in lmRunEntries) {
                    entry.EntryType = StartupEntryType.CurrentUserRun;
                }
                entries.AddRange(lmRunEntries);
            }
            if ((types & StartupEntryType.LocalMachineRun) == StartupEntryType.LocalMachineRun) {
                store = LaunchMeNotBLL.GetStore(StartupEntryType.LocalMachineRun);
                var currentUserRunEntries = store.GetStartupEntries();
                foreach (var entry in currentUserRunEntries) {
                    entry.EntryType = StartupEntryType.LocalMachineRun;
                }
                entries.AddRange(currentUserRunEntries);
            }
            if ((types & StartupEntryType.LaunchMeNot) == StartupEntryType.LaunchMeNot) {
                store = LaunchMeNotBLL.GetStore(StartupEntryType.LaunchMeNot);
                var launchMeNotEntries = store.GetStartupEntries();
                foreach (var entry in launchMeNotEntries) {
                    entry.EntryType = StartupEntryType.LaunchMeNot;
                }
                entries.AddRange(launchMeNotEntries);
            }
            return entries;
        }
        #endregion

        /// <summary>
        /// Filters entries into collections of their type.
        /// </summary>
        /// <param name="entries">The entries.</param>
        /// <returns>A filtered collection of the entries.</returns>
        private static StartupEntryCollection FilterEntries(IEnumerable<IStartupEntry> entries) {
            List<IStartupEntry> allUsersStartmenuEntries = new List<IStartupEntry>(),
                                currentUserStartmenuEntries = new List<IStartupEntry>(),
                                hklmRunEntries = new List<IStartupEntry>(),
                                hkcuRunEntries = new List<IStartupEntry>(),
                                launchMeNotEntries = new List<IStartupEntry>();
            foreach(IStartupEntry entry in entries) {
                switch (entry.EntryType) {
                    case StartupEntryType.AllUsersStartup:
                        allUsersStartmenuEntries.Add(entry);
                        break;
                    case StartupEntryType.CurrentUserStartup:
                        currentUserStartmenuEntries.Add(entry);
                        break;
                    case StartupEntryType.CurrentUserRun:
                        hkcuRunEntries.Add(entry);
                        break;
                    case StartupEntryType.LocalMachineRun:
                        hklmRunEntries.Add(entry);
                        break;
                    case StartupEntryType.LaunchMeNot:
                        launchMeNotEntries.Add(entry);
                        break;
                }
            }
            return new StartupEntryCollection(allUsersStartmenuEntries, currentUserStartmenuEntries, hklmRunEntries, hkcuRunEntries, launchMeNotEntries);
        }

        /// <summary>
        /// A collection of all startup entries on the system.
        /// </summary>
        public class StartupEntryCollection
        {
            public IEnumerable<IStartupEntry> AllUsersStartmenu { get; private set; }
            public IEnumerable<IStartupEntry> CurrentUserStartmenu { get; private set; }
            public IEnumerable<IStartupEntry> LocalMachineRunEntries { get; private set; }
            public IEnumerable<IStartupEntry> CurrentUserRun { get; private set; }
            public IEnumerable<IStartupEntry> LaunchMeNot { get; private set; }

            public StartupEntryCollection(IEnumerable<IStartupEntry> allUsersStartmenuEntries,
                                        IEnumerable<IStartupEntry> currentUserStartmenuEntries,
                                        IEnumerable<IStartupEntry> localMachineRunEntries,
                                        IEnumerable<IStartupEntry> currentUserRunEntries,
                                        IEnumerable<IStartupEntry> launchMeNotEntries) {
                this.AllUsersStartmenu = allUsersStartmenuEntries;
                this.CurrentUserStartmenu = currentUserStartmenuEntries;
                this.LocalMachineRunEntries = localMachineRunEntries;
                this.CurrentUserRun = currentUserRunEntries;
                this.LaunchMeNot = launchMeNotEntries;
            }
        }
    }
}